/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.client.model;

import offset.nodes.client.virtual.model.SchemaProperty;
import offset.nodes.client.virtual.model.SchemaNode;
import offset.nodes.client.virtual.model.SchemaItem;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import javax.swing.JTree;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import offset.nodes.client.virtual.model.jcr.QName;
import offset.nodes.client.virtual.model.jcr.nodetype.NodeDefinition;
import offset.nodes.client.virtual.model.jcr.nodetype.NodeTypeDefinition;
import offset.nodes.client.virtual.model.jcr.nodetype.PropertyDefinition;

/**
 * Implements the model of a hierarchical schema tree.
 *
 * Such a tree will resolve all children with their type, i.e. their properties and children. That way, a
 * schema tree with a depth greater 2 may be created.
 * @author Walter Lütgenau
 */
public class HierarchyTree extends AbstractSchemaTree {
    
    /** Creates a new instance of HierarchyTree */
    public HierarchyTree(JTree tree) {
        this.setTree(tree);
        this.setRoot((DefaultMutableTreeNode) ((DefaultTreeModel)tree.getModel()).getRoot());
    }
    
    
    public void removeType(DefaultMutableTreeNode node) {
        if( !(node.getUserObject() instanceof SchemaNode) || ((SchemaNode)node.getUserObject()).getNodeType() == null )
            return; // remove types only
        
        final NodeTypeDefinition type = ((SchemaNode)node.getUserObject()).getNodeType();
        
        processNode( getRoot(), new Processor() {
            public void process(DefaultMutableTreeNode remove, HierarchyTree.ChildIterator iterator) {
                if ( hasType( remove, type)) {
                    DefaultMutableTreeNode parent = (DefaultMutableTreeNode) remove.getParent();
                    
                    ((DefaultTreeModel) getTree().getModel()).removeNodeFromParent(remove);
                    
                    return;
                }
                
                iterator.incrementIndex();
            }
            
        });
    }
    
    
    public void removeChildFromType(DefaultMutableTreeNode parent, DefaultMutableTreeNode child) {
        if( !(parent.getUserObject() instanceof SchemaNode) || ((SchemaNode)parent.getUserObject()).getNodeType() == null ||
            !(child.getUserObject() instanceof SchemaItem))
            return; // remove from parent types only
        
        final NodeTypeDefinition type = ((SchemaNode)parent.getUserObject()).getNodeType();
        final QName name = ((SchemaItem) child.getUserObject()).getName();
        
        processNode( getRoot(), new Processor() {
            public void process(DefaultMutableTreeNode remove, HierarchyTree.ChildIterator iterator) {
                DefaultMutableTreeNode currentParent = (DefaultMutableTreeNode) remove.getParent();
                
                if ( hasType( currentParent, type) && hasName(remove, name)) {
                    ((DefaultTreeModel) getTree().getModel()).removeNodeFromParent(remove);
                    
                    return;
                }
                
                iterator.incrementIndex();
            }
            
        });
    }
    
    
    public void addType(DefaultMutableTreeNode type, int position) {
        if( !(type.getUserObject() instanceof SchemaNode) || ((SchemaNode)type.getUserObject()).getNodeType() == null )
            return; // insert type nodes only
        
        ((DefaultTreeModel) getTree().getModel()).insertNodeInto(type, getRoot(), position );
    }
    
    public void addChildToType(DefaultMutableTreeNode parent, final DefaultMutableTreeNode child, final int position) {
        if( !(parent.getUserObject() instanceof SchemaNode) || ((SchemaNode)parent.getUserObject()).getNodeType() == null )
            return; // remove from parent types only
        
        final NodeTypeDefinition type = ((SchemaNode)parent.getUserObject()).getNodeType();
        
        processNode( (DefaultMutableTreeNode) getRoot(), new Processor() {
            public void process(DefaultMutableTreeNode node, HierarchyTree.ChildIterator iterator) {
                DefaultMutableTreeNode currentParent = (DefaultMutableTreeNode) node.getParent();
                
                if ( hasType( node, type) )
                    ((DefaultTreeModel) getTree().getModel()).insertNodeInto(child, node, position );
                
                iterator.incrementIndex();
            }
            
        });
    }
    
    public static DefaultMutableTreeNode buildTree(NodeTypes types, NodeTypeDefinition[] definitions) {
        HashSet roots = new HashSet();
        
        for( int i=0; i<definitions.length; i++) {
            roots.add(definitions[i].getName());
        }
        
        // extract roots, i.e. node type definitions that are not child nodes in another node type definition
        for( int i=0; i<definitions.length; i++) {
            NodeTypeDefinition parent = definitions[i];
            NodeDefinition[] children = parent.getDeclaredChildNodeDefinitions();
            for( int j=0; j<children.length; j++) {
                QName required = children[j].getRequiredPrimaryTypes()[0];
                if ( roots.contains(required))
                    roots.remove(required);
            }
        }
        
        // sort the root nodes according to name
        QName[] rootArray = (QName[]) roots.toArray(new QName[roots.size()]);
        List rootList = new ArrayList();
        for( int i=0; i<rootArray.length; i++)
            rootList.add(rootArray[i]);
        Collections.sort(rootList, new Comparator() {
            public int compare(Object o1, Object o2) {
                QName n1 = (QName) o1;
                QName n2 = (QName) o2;
                return n1.getLocalName().compareTo(n2.getLocalName());
            }
        });
        
        // build hierarchical tree
        DefaultMutableTreeNode root = new DefaultMutableTreeNode();
        Iterator it = rootList.iterator();
        while( it.hasNext()) {
            QName name = (QName) it.next();
            NodeTypeDefinition node = types.get(name);
            root.add( setupHierarchicalTree( name, node, null, types));
        }
        
        return root;
    }
    
    public static DefaultMutableTreeNode setupHierarchicalTree( QName name, NodeTypeDefinition type, NodeDefinition node, NodeTypes types) {
        return setupHierarchicalTree(name, type, node, types, true, false, true);
    }

    public static DefaultMutableTreeNode setupHierarchicalTree( QName name, NodeTypeDefinition type, NodeDefinition node, NodeTypes types, boolean addProperties) {
        return setupHierarchicalTree(name, type, node, types, addProperties, false, true);
    }

    public static DefaultMutableTreeNode setupHierarchicalTree( QName name, NodeTypeDefinition type, NodeDefinition node, NodeTypes types, boolean addProperties, boolean addSuperTypeProperties) {
        return setupHierarchicalTree(name, type, node, types, addProperties, addSuperTypeProperties, true);
    }
    
    public static DefaultMutableTreeNode setupHierarchicalTree( QName name, NodeTypeDefinition type, NodeDefinition node, NodeTypes types, boolean addProperties, boolean addSuperTypeProperties, boolean addSelf) {
        DefaultMutableTreeNode parent = new DefaultMutableTreeNode();

        parent = setupHierarchicalTree(parent, name, type, node, types, addProperties, addSuperTypeProperties);
        parent.setParent(null);

        return parent;
    }

    public static DefaultMutableTreeNode setupHierarchicalTree( DefaultMutableTreeNode parent, QName name, NodeTypeDefinition type, NodeDefinition node, NodeTypes types, boolean addProperties, boolean addSuperTypeProperties) {
        QName displayName = null;
        if (name.equals(QName.ANY_NAME))
            displayName = type.getName();
        else
            displayName = name;

        DefaultMutableTreeNode self = new DefaultMutableTreeNode(new SchemaNode(displayName, node, type));

        parent.add(self);

        if ( addProperties )
            addProperties(name, self, type, types, addSuperTypeProperties);

        if (!inPath(SimpleNamespaceRegistry.getInstance().toPrefixName(displayName), parent))
            addChildren(self, type, types, addProperties, addSuperTypeProperties);


        return self;
    }

    /**
     * In case of residual name, use the type name for display
     * 
     * @param name
     * @param type
     * @return
     */
    protected static QName displayName(QName name, NodeTypeDefinition type) {
        if (name.equals(QName.ANY_NAME))
            return type.getName();
        return name;
    }

    /**
     * Add the properties of this type and its recursive supertypes to the tree node.
     * 
     * @param name
     * @param parent
     * @param type
     * @param types
     * @param addSuperTypeProperties
     */
    protected static void addProperties(QName name, DefaultMutableTreeNode parent, NodeTypeDefinition type, NodeTypes types, boolean addSuperTypeProperties) {
        if (name != null && name.equals(QName.ANY_NAME))
            parent.add(new DefaultMutableTreeNode(new SchemaProperty(QName.NAME_PROPERTY, null)));

        PropertyDefinition[] properties = type.getDeclaredPropertyDefinitions();
        for (int i = 0; i < properties.length; i++) {
            parent.add(new DefaultMutableTreeNode(new SchemaProperty(properties[i].getName(), properties[i])));
        }

        if (addSuperTypeProperties) {
            for (int i = 0; i < type.getSupertypes().length; i++) {
                NodeTypeDefinition superType = types.get(type.getSupertypes()[i]);
                if (superType != null)
                    addProperties(null, parent, superType, types, addSuperTypeProperties);
            }
        }
    }

    /**
     * Add the children and optionally recursively the children of the supertypes.
     * 
     * @param parent
     * @param type
     * @param types
     * @param addSelf
     * @param addProperties
     * @param addSupertypes
     */
    protected static void addChildren(DefaultMutableTreeNode parent, NodeTypeDefinition type, NodeTypes types, boolean addProperties, boolean addSupertypes) {
        NodeDefinition[] children = type.getDeclaredChildNodeDefinitions();

        for( int i=0; i<children.length; i++) {
            NodeTypeDefinition childType = types.get(children[i].getRequiredPrimaryTypes()[0]);
            if ( childType != null) 
                parent.add(setupHierarchicalTree(parent, children[i].getName(), childType, children[i], types, addProperties, addSupertypes));
        }

        if (addSupertypes) {
            for (QName superTypeName : type.getSupertypes()) {
                NodeTypeDefinition superType = types.get(superTypeName);
  
                addChildren(parent, superType, types, addProperties, addSupertypes);
            }
       }
    }

    /**
     * Is a node of the name already in the path?
     *
     * @param name
     * @param path
     * @return
     */
    protected static boolean inPath(String name, DefaultMutableTreeNode path) {
        while (path.getUserObject() != null) {
            if (path.toString().equals(name))
                return true;

            path = (DefaultMutableTreeNode) path.getParent();
        }

        return false;
    }
    
    
}
